import asyncio
from asyncio import create_task
from inspect import iscoroutine
from typing import Optional, Generator, Callable

from bald_spider.core.downloader import DownloaderBase
from bald_spider.core.processor import Processor
from bald_spider.core.scheduler import Scheduler
from bald_spider.event import spider_opened, spider_error, request_scheduled
from bald_spider.exceptions import OutPutTypeError
from bald_spider.http.request import Request
from bald_spider.items.items import Item
from bald_spider.spider import Spider
from bald_spider.utils.log import get_logger
from bald_spider.utils.project import load_class
from bald_spider.utils.spider import transform
from bald_spider.task_manager import TaskManager


class Engine:
    def __init__(self, crawler):
        self.logger = get_logger(self.__class__.__name__)
        self.crawler = crawler
        self.settings = crawler.settings
        self.downloader: Optional[DownloaderBase] = None
        self.start_requests: Optional[Generator] = None
        self.scheduler: Optional[Scheduler] = None
        self.spider: Optional[Spider] = None
        self.processor: Optional[Processor] = None
        self.task_manager: Optional[TaskManager] = TaskManager(self.settings.get("CONCURRENCY"))
        self.running: bool = False
        self.normal: bool = True

    def engine_start(self):
        self.running = True
        self.logger.info(
            f"bald_spider(version:{self.settings.get('VERSION')}) started. "
            f"(project name: {self.settings.get('PROJECT_NAME')})"
        )

    async def start_spider(self, spider):
        self.spider = spider
        self.scheduler = Scheduler.create_instance(self.crawler)
        if hasattr(self.scheduler, 'open'):
            self.scheduler.open()
        self.processor = Processor(self.crawler)
        if hasattr(self.processor, 'open'):
            self.processor.open()
        downloader_cls = self._get_downloader_cls()
        self.downloader = downloader_cls.create_instance(self.crawler)
        if hasattr(self.downloader, 'open'):
            self.downloader.open()
        self.start_requests = iter(spider.start_requests())
        await self._open_spider()

    def _get_downloader_cls(self):
        downloader_cls = load_class(self.settings.get('DOWNLOADER'))
        if not issubclass(downloader_cls, DownloaderBase):
            raise TypeError(f"The downloader class ({self.settings.get('DOWNLOADER')})"
                            f"doesn't fully implement required interface")
        return downloader_cls

    async def _open_spider(self):
        asyncio.create_task(self.crawler.subscriber.notify(spider_opened))
        crawling = asyncio.create_task(self.csrf())
        # 这里可以进行下一步操作
        # asyncio.create_task(self.scheduler.interval_log(self.settings.getint('INTERVAL')))
        await crawling

    async def csrf(self):
        """主逻辑"""
        while self.running:
            # 出队判断是否存在队列
            if (request := await self._get_next_request()) is not None:
                await self._crawl(request)
            else:
                try:
                    start_request = next(self.start_requests)
                except StopIteration:
                    self.start_requests = None
                except Exception as e:
                    if not await self._exit():
                        continue
                    self.running = False
                    if self.start_requests is not None:
                        self.logger.info(f"Error during start_requests: {e}")
                    break
                else:  # 入队
                    await self.enqueue_request(start_request)
        if not self.running:
            await self.close_spider()

    async def _crawl(self, request):
        # 实现并发
        async def crawl_task():
            outputs = await self._fetch(request)
            # 处理outputs
            if outputs:
                await self._handle_spider_output(outputs)

        await self.task_manager.semaphore.acquire()
        self.task_manager.create_task(crawl_task())

    async def _fetch(self, request):
        async def _success(_response):
            callback: Callable = request.callback or self.spider.parse

            if _outputs := callback(_response):
                if iscoroutine(_outputs):
                    await _outputs
                else:
                    return transform(_outputs,_response)

        _response = await self.downloader.fetch(request)
        if _response is None:
            return None
        outputs = await _success(_response)
        return outputs

    async def enqueue_request(self, request):
        await self._scheduler_request(request)

    async def _scheduler_request(self, request):
        if await self.scheduler.enqueue_request(request):
            create_task(self.crawler.subscriber.notify(request_scheduled, request, self.crawler.spider))

    async def _get_next_request(self):
        return await self.scheduler.next_request()

    async def _handle_spider_output(self, outputs):
        async for output in outputs:
            if isinstance(output, (Request, Item)):
                await self.processor.enqueue(output)
            elif isinstance(output,Exception):
                asyncio.create_task(self.crawler.subscriber.notify(spider_error, output,self.spider))
                raise output
            else:
                raise OutPutTypeError(f"{type(self.spider)} must return `Request` or `Item`")

    async def _exit(self):
        if self.downloader.idle() and self.scheduler.idle() and self.task_manager.all_done() and self.processor.idle():
            return True
        return False

    async def close_spider(self):
        await asyncio.gather(*self.task_manager.current_task)
        await self.downloader.closer()
        await self.scheduler.close()
        if self.normal:
            await self.crawler.close()
